﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.Serialization;
using System.Reflection;
using System.Runtime.Serialization.Json;
using System.Xml;
using Templay.Attributes;
using System.Runtime.InteropServices;

namespace Templay
{
    /// <summary>
    /// Tag class to bind to template
    /// </summary>
    [Serializable]
    public abstract class Tag
    {

        #region Properties

        /// <summary>
        /// Id of the tag
        /// </summary>
        public string Id { get; set; }

        /// <summary>
        /// Auto generated guid of the tag
        /// </summary>
        public Guid Guid { get; internal set; }

        /// <summary>
        /// Parent tag, null if it doesn't have parent
        /// </summary>
        public Tag Parent { get; set; }

        /// <summary>
        /// Style(s) of the tag
        /// </summary>
        public string Style { get; set; }

        /// <summary>
        /// Children Tag/Tag of this Tag
        /// </summary>
        public List<Tag> Childrens { get; internal set; }

        #endregion

        #region Constructor

        public Tag()
        {
            if (Guid == Guid.Empty)
                Guid = Guid.NewGuid();

            Style = "";
            Childrens = new List<Tag>();
        }

        #endregion

        #region Public methods

        public virtual void Initialize() { }
        public virtual void InitProcess() { }
        public virtual void Process(params object[] parameters) { }

        /// <summary>
        /// Run all children process method non recursively
        /// </summary>
        /// <param name="parameters">Customs parameters</param>
        public void ProcessChildrens(params object[] parameters)
        {
            Childrens.ForEach(x => x.Process(parameters));
        }

        /// <summary>
        /// Substitutes all Process method to another interface method for better abstraction of Templay
        /// It run the method non recursively.
        /// </summary>
        /// <typeparam name="TInterface">The interface you need use</typeparam>
        /// <param name="methodChildren">The interface method to substitute</param>
        public void ProcessChildrenBy<TInterface>(Action<TInterface> methodChildren) where TInterface : class
        {
            foreach (Tag children in Childrens)
            {
                if (children is TInterface)
                    methodChildren(children as TInterface);
            }
        }

        #endregion

        #region Internal methods

        /// <summary>
        /// Check the required properties which don't been setted
        /// </summary>
        internal void FetchRequiredProperties()
        {
            Type thisType = this.GetType();
            var propertiesRequired = thisType.GetProperties().Where(x => x.GetCustomAttributes(true)
                                                  .Any(y => y is RequiredAttribute));

            foreach (PropertyInfo property in propertiesRequired)
            {
                object value = property.GetValue(this, null);

                if (value == null)
                    throw new Exception(String.Format("The property {0} for the tag {1} is required", property.Name, thisType.Name));

            }
        }

        /// <summary>
        /// Set tag property
        /// </summary>
        /// <param name="name">Name of the property</param>
        /// <param name="value">Value of the property</param>
        /// <param name="externalReferences">externals references from the current template</param>
        internal void SetProperty(string name, object value, IDictionary<string, int> externalReferences)
        {
            //Get the property of this name
            PropertyInfo property = this.GetType().GetProperty(name);

            //No matching property : return
            if (property == null)
                return;

            //Get value to string
            string strValue = value.ToString();

            //Attribute process
            object[] attributes = property.GetCustomAttributes(true);

            foreach (object attribute in attributes)
            {
                if (attribute is ProcessAttribute)
                    value = (attribute as ProcessAttribute).provider.Process(value);
            }

            //Allocation internal reference if Id is defined
            if (name == "Id") 
            {
                GCHandle handle = GCHandle.Alloc(this);
                IntPtr pointer = GCHandle.ToIntPtr(handle);
                int intPointer = pointer.ToInt32();

                if (!externalReferences.ContainsKey(strValue))
                    externalReferences.Add(strValue, intPointer);
                else
                    externalReferences[strValue] = intPointer;
            }

            //Get internals / externals reference
            if (strValue.Length > 5 && strValue.ToLower().StartsWith("#ref:"))
            {
                string reference = strValue.Replace("#ref:", "");

                if (externalReferences.ContainsKey(reference))
                {
                    GCHandle handle = GCHandle.FromIntPtr(new IntPtr(externalReferences[reference]));
                    value = handle.Target;
                }
                else
                    throw new Exception(String.Format("The reference '{0}' doesn't exist", reference));

            }

            //Get json
            if (strValue.Length > 6 && strValue.ToLower().StartsWith("#json:"))
            {
                DataContractJsonSerializer serializer = new DataContractJsonSerializer(property.PropertyType);

                string json = strValue.Replace('\'', '"').Replace("#json:", "");
                byte[] jsonBytes = json.ToByteArray();

                XmlDictionaryReader reader = JsonReaderWriterFactory.CreateJsonReader(jsonBytes, System.Xml.XmlDictionaryReaderQuotas.Max);
                value = serializer.ReadObject(reader);
            }
            else
                value = Convert.ChangeType(value, property.PropertyType); // Change to expected type

            //Set the value
            property.SetValue(this, value, null);

        }

        #endregion

    }
}
